﻿using System;
using System.ComponentModel;
using System.Globalization;
using System.Xml.Linq;

namespace System.Data
{
    /// <summary>
    /// DataField state
    /// </summary>
    public enum DataFieldState
    {
        Modified,
        Unchanged
    }

    /// <summary>
    /// DataField version
    /// </summary>
    public enum DataFieldVersion
    {
        Current,
        Original,
        Proposed
    }

    /// <summary>
    /// Provides DataField functionality
    /// </summary>
    public class DataField
    {
        private const string NonValidDataType = "\"{0}\" is not valid value for \"{1}\" data type";
        private const string FieldIsRequired = "Filed is required: {0}";
        private const string UnrecognizedDataType = "Unrecognized data type: ";

        #region Data Fields
        /// <summary>
        /// "CurrentValue" property data field
        /// </summary>
        private String _currentValue;

        /// <summary>
        /// "FieldError" property data field
        /// </summary>
        private string _fieldError;

        /// <summary>
        /// "FieldState" property data field
        /// </summary>
        private DataFieldState _state;

        /// <summary>
        /// "FieldVersion" property data field
        /// </summary>
        private DataFieldVersion _fieldVersion;

        /// <summary>
        /// Holds field version before it became Proposed
        /// </summary>
        private DataFieldVersion _fieldVersionBeforeProposed;

        /// <summary>
        /// "HasChanges" property data field
        /// </summary>
        private bool _hasChanges;

        /// <summary>
        /// "HasError" property data field
        /// </summary>
        private bool _hasError;

        /// <summary>
        /// "Index" property data field
        /// </summary>
        private int _index;

        /// <summary>
        /// "InEditMode" property data field
        /// </summary>
        private bool _inEditMode;

        /// <summary>
        /// "OriginalValue" property data field
        /// </summary>
        private String _originalValue;

        /// <summary>
        /// "ProposedValue" property data field
        /// </summary>
        private String _proposedValue;

        /// <summary>
        /// "Row" property data field
        /// </summary>
        private DataRow _row;

        /// <summary>
        /// "UseCurrentCulture" property data field
        /// </summary>
        private bool _useCurrentCulture;
        #endregion

        #region Events
        /// <summary>
        /// Is raised before data was changed
        /// </summary>
        internal event EventHandler<DataSetEventArgs> FieldValueChanging;

        /// <summary>
        /// INotifyPropertyChanged interface implementation
        /// </summary>
        internal event PropertyChangedEventHandler PropertyChanged;
        #endregion

        #region Constructors
        /// <summary>
        /// Default constructor
        /// </summary>
        internal DataField()
        {
            this._useCurrentCulture = true;
            this._state = DataFieldState.Unchanged;
            this._fieldVersion = DataFieldVersion.Original;
            this._fieldVersionBeforeProposed = DataFieldVersion.Original;
        }

        /// <summary>
        /// Makes DataField instance
        /// </summary>
        /// <param name="index">Column index</param>
        /// <param name="row">Data row</param>
        internal DataField(int index, DataRow row) : this()
        {
            // Validate arguments
            if (index < 0)
            {
                throw new ArgumentOutOfRangeException("index");
            }

            if (row == null)
            {
                throw new ArgumentNullException("row");
            }

            this._index = index;
            this._row = row;
        }

        /// <summary>
        /// DataField destructor
        /// </summary>
        ~DataField()
        {
            this._row = null;
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets column name
        /// </summary>
        public string ColumnName
        {
            get
            {
                return this._row.Table.Columns[this._index].ColumnName;
            }
        }

        /// <summary>
        /// Gets current value
        /// </summary>
        internal String CurrentValue
        {
            get
            {
                return this._currentValue;
            }
        }

        /// <summary>
        /// Gets field error
        /// </summary>
        public string FieldError
        {
            get
            {
                return this._fieldError;
            }
        }

        /// <summary>
        /// Gets "HasError" flag value
        /// </summary>
        public bool HasError
        {
            get
            {
                return this._hasError;
            }
        }

        /// <summary>
        /// Gets DataField index
        /// </summary>
        internal int Index
        {
            get
            {
                return this._index;
            }
        }

        /// <summary>
        /// Gets value which indicates whether DataField is in Edit mode
        /// </summary>
        internal bool InEditMode
        {
            get
            {
                return this._inEditMode;
            }
        }

        /// <summary>
        /// Gets original value
        /// </summary>
        internal String OriginalValue
        {
            get
            {
                return this._originalValue;
            }
        }

        /// <summary>
        /// Gets proposed value
        /// </summary>
        internal String ProposedValue
        {
            get
            {
                return this._proposedValue;
            }
        }

        /// <summary>
        /// Gets type name
        /// </summary>
        public string TypeName
        {
            get
            {
                return this._row.Table.Columns[this._index].DataType.Name;
            }
        }

        /// <summary>
        /// Sets flag indicating current culture usage
        /// </summary>
        internal bool UseCurrentCulture
        {
            set
            {
                this._useCurrentCulture = value;
            }
        }

        /// <summary>
        /// Gets or sets field value
        /// </summary>
        public string Value
        {
            get
            {
                switch (this._fieldVersion)
                {
                    case DataFieldVersion.Current:
                        if (this._useCurrentCulture)
                        {
                            return DataField.ToCurrentCulture(this._currentValue, TypeName);
                        }
                        else
                        {
                            return this._currentValue;
                        }
                    case DataFieldVersion.Original:
                        if (this._useCurrentCulture)
                        {
                            return DataField.ToCurrentCulture(this._originalValue, TypeName);
                        }
                        else
                        {
                            return this._originalValue;
                        }
                    case DataFieldVersion.Proposed:
                        if (this._useCurrentCulture)
                        {
                            return DataField.ToCurrentCulture(this._proposedValue, TypeName);
                        }
                        else
                        {
                            return this._proposedValue;
                        }
                }

                throw new InvalidOperationException("Unrecognized field version");
            }
            set
            {
                // _useCurrentCulture is False during DataSet restoration from xml
                if (this._useCurrentCulture)
                {
                    value = this.ToInvariantCulture(value, TypeName);
                }

                // Return to original value if current value is the same as original
//                if (value == this._originalValue || value == String.Empty && this._originalValue == null)
                if (value == this._originalValue)
                {
                    this.RejectChanges();
                    return;
                }

                //// Do nothing if value is the same in original row
                //if (this._fieldVersion == DataFieldVersion.Original && this._currentValue == value)
                //{
                //    return;
                //}

                this.BeginEdit();

                // Set proposed value
                this._proposedValue = value;

                // Validate required field
                if (string.IsNullOrEmpty(this._proposedValue))
                {
                    // Value is null or empty string
                    if (this._row.Table.Columns[this._index].AllowDBNull)
                    {
                        // Do nothing, no need to validate data type
                    }
                    else
                    {
                        // Set required field error
                        this.SetFieldError(String.Format(CultureInfo.CurrentCulture, FieldIsRequired, this._row.Table.Columns[this._index].ColumnName));
                    }
                }
                else
                {
                    // Validate data type
                    if (DataField.ValidateDataType(ref this._proposedValue, this._row.Table.Columns[this._index].DataType.FullName, this._useCurrentCulture))
                    {
                        // Validate required field if null value is set
                        if (string.IsNullOrEmpty(this._row.Table.Columns[this._index].NullValue))
                        {
                            this.SetFieldError(null);
                        }
                        else
                        {
                            if (this._row.Table.Columns[this._index].NullValue == this._proposedValue)
                            {
                                // Set required field error
                                this.SetFieldError(String.Format(CultureInfo.CurrentCulture, FieldIsRequired, this._row.Table.Columns[this._index].ColumnName));
                            }
                            else
                            {
                                this.SetFieldError(null);
                            }
                        }
                    }
                    else
                    {
                        this.SetFieldError(String.Format(CultureInfo.CurrentCulture, NonValidDataType, this._proposedValue, this._row.Table.Columns[this._index].DataType.FullName));
                    }
                }

                // Raise Changing event
                if (FieldValueChanging != null)
                {
                    DataSetEventArgs dataSetEventArgs = new DataSetEventArgs(this._proposedValue);
                    FieldValueChanging(this, dataSetEventArgs);
                }

                this.OnPropertyChanged(this.ColumnName);
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Commits all the changes made since the last time AcceptChanges was called
        /// </summary>
        internal void AcceptChanges()
        {
            this.EndEdit();
            if (this._hasChanges)
            {
                this._originalValue = this._currentValue;
                this._proposedValue = null;
                this._hasChanges = false;
                this._state = DataFieldState.Unchanged;
                this._fieldVersion = DataFieldVersion.Original;
            }
        }

        /// <summary>
        /// Starts edit operation
        /// </summary>
        internal void BeginEdit()
        {
            if (!this._inEditMode)
            {
                // Put field in edit mode
                this._inEditMode = true;
                this._hasChanges = true;

                // Cache field verion but only once
                if (this._fieldVersionBeforeProposed == DataFieldVersion.Original)
                {
                    this._fieldVersionBeforeProposed = this._fieldVersion;
                }

                // Change FieldState from Unchanged to Modified
                //if (this._state == DataFieldState.Unchanged)
                //{
                    this.SetModified();
                //}
            }
        }

        /// <summary>
        /// Cancels edit operation
        /// </summary>
        internal void CancelEdit()
        {
            if (this._inEditMode)
            {
                // Cancel editing
                this._inEditMode = false;

                // Clear proposed value
                this._proposedValue = null;

                // Restore field version from cache
                this._fieldVersion = this._fieldVersionBeforeProposed;
            }
        }

        /// <summary>
        /// Changes data type. Value is set to null if it's not compatible with new type
        /// </summary>
        /// <param name="proposedValue">Data type name</param>
        internal void ChangeType(string proposedValue)
        {
            if (!DataField.ValidateDataType(ref this._currentValue, proposedValue, this._useCurrentCulture))
            {
                this._currentValue = null;
            }

            if (!DataField.ValidateDataType(ref this._originalValue, proposedValue, this._useCurrentCulture))
            {
                this._originalValue = null;
            }

            if (!DataField.ValidateDataType(ref this._proposedValue, proposedValue, this._useCurrentCulture))
            {
                this._proposedValue = null;
            }
        }

        /// <summary>
        /// Copies DataFeild
        /// </summary>
        /// <param name="dataField">Target DataField</param>
        internal void Copy(DataField dataField)
        {
            dataField._currentValue = this._currentValue;
            dataField._fieldError = this._fieldError;
            dataField._state = this._state;
            dataField._fieldVersion = this._fieldVersion;
            dataField._fieldVersionBeforeProposed = this._fieldVersionBeforeProposed;
            dataField._hasChanges = this._hasChanges;
            dataField._hasError = this._hasError;
            dataField._inEditMode = this._inEditMode;
            dataField._originalValue = this._originalValue;
            dataField._proposedValue = this._proposedValue;
        }

        /// <summary>
        /// Ends edit operation
        /// </summary>
        internal void EndEdit()
        {
            if (this._inEditMode)
            {
                this._currentValue = this._proposedValue;
                this._inEditMode = false;
                this._state = DataFieldState.Modified;
                this._fieldVersion = DataFieldVersion.Current;
                this._hasChanges = true;
                this.OnPropertyChanged(this.ColumnName);
            }
        }

        /// <summary>
        /// Raises PropertChanged event
        /// </summary>
        /// <param name="propertyName">Property name</param>
        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        internal void RecalculateErrors()
        {
            string value = null;
            switch (this._fieldVersion)
            {
                case DataFieldVersion.Current:
                    value = this._currentValue;
                    break;
                case DataFieldVersion.Original:
                    value = this._originalValue;
                    break;
                case DataFieldVersion.Proposed:
                    value = this._proposedValue;
                    break;
            }

            // Validate value
            if (string.IsNullOrEmpty(value))
            {
                // Value is null or empty string
                // Validate required field
                if (this._row.Table.Columns[this._index].AllowDBNull)
                {
                    this.SetFieldError(null);
                }
                else
                {
                    // Set required field error
                    this.SetFieldError(String.Format(CultureInfo.CurrentCulture, FieldIsRequired, this._row.Table.Columns[this._index].ColumnName));
                }
            }
            else
            {
                // Validate data type
                if (DataField.ValidateDataType(ref value, this._row.Table.Columns[this._index].DataType.FullName, this._useCurrentCulture))
                {
                    // Validate required field if null value is set
                    if (string.IsNullOrEmpty(this._row.Table.Columns[this._index].NullValue))
                    {
                        this.SetFieldError(null);
                    }
                    else
                    {
                        if (this._row.Table.Columns[this._index].NullValue == value)
                        {
                            // Set required field error
                            this.SetFieldError(String.Format(CultureInfo.CurrentCulture, FieldIsRequired, this._row.Table.Columns[this._index].ColumnName));
                        }
                        else
                        {
                            this.SetFieldError(null);
                        }
                    }
                }
                else
                {
                    this.SetFieldError(String.Format(CultureInfo.CurrentCulture, NonValidDataType, value, this._row.Table.Columns[this._index].DataType.FullName));
                }
            }
        }

        /// <summary>
        /// Rejects all changes made since AcceptChanges was last called
        /// </summary>
        internal void RejectChanges()
        {
            this.CancelEdit();
            if (this._hasChanges)
            {
                this._currentValue = this._originalValue;
                this._proposedValue = null;
                this._hasChanges = false;
                this._state = DataFieldState.Unchanged;
                this._fieldVersion = DataFieldVersion.Original;
                this.RecalculateErrors();
                this.OnPropertyChanged(this.ColumnName);
            }
        }

        /// <summary>
        /// Sets field error
        /// </summary>
        /// <param name="errorMessage">Error message</param>
        private void SetFieldError(string errorMessage)
        {
            bool calOnPropertyChanged = false;
            if (this._fieldError != errorMessage)
            {
                calOnPropertyChanged = true;
            }

            this._fieldError = errorMessage;
            if (string.IsNullOrEmpty(errorMessage))
            {
                this._hasError = false;
            }
            else
            {
                this._hasError = true;
            }

            if (calOnPropertyChanged)
            {
                OnPropertyChanged(this.ColumnName + "HasError");
                OnPropertyChanged(this.ColumnName + "Error");
            }
        }

        /// <summary>
        /// Sets dataField state as Modified
        /// </summary>
        private void SetModified()
        {
            this._state = DataFieldState.Modified;
            this._fieldVersion = DataFieldVersion.Proposed;
        }

        /// <summary>
        /// Formats value using invariant culture settings
        /// </summary>
        /// <param name="value">Invariant culture value</param>
        /// <param name="dataTypeName">Data type name</param>
        /// <returns>Current culture value</returns>
        private string ToInvariantCulture(string value, string dataTypeName)
        {
            // Do nothing if there is no value for conversion
            if (string.IsNullOrEmpty(value))
            {
                return value;
            }

            // Convert value from current to invariant culture
            switch (dataTypeName)
            {
                case "Boolean":
                    return value.ToLower(CultureInfo.InvariantCulture);
                case "DateTime":
                    // DateTime case is slightly different because the inner format must be compatible with xsd
                    // That's why id does conversion AND formatting even so converion is not used when external
                    // converter takes care about it
                    DateTime dateTimeValue;
                    if (DateTime.TryParse(value.ToString(), CultureInfo.CurrentCulture, DateTimeStyles.None, out dateTimeValue))
                    {
                        // Inner format compatible with xsd
                        return dateTimeValue.ToString("yyyy-MM-ddThh:mm:ss", CultureInfo.InvariantCulture);
                    }

                    if (DateTime.TryParse(value.ToString(), CultureInfo.InvariantCulture, DateTimeStyles.None, out dateTimeValue))
                    {
                        // Inner format compatible with xsd
                        return dateTimeValue.ToString("yyyy-MM-ddThh:mm:ss", CultureInfo.InvariantCulture);
                    }

                    this.SetFieldError(String.Format(CultureInfo.CurrentCulture, NonValidDataType, value, this._row.Table.Columns[this._index].DataType.FullName));
                    break;
                case "Decimal":
                    Decimal decimalValue;
                    if (Decimal.TryParse(value, NumberStyles.Number, CultureInfo.CurrentCulture, out decimalValue))
                    {
                        return decimalValue.ToString(CultureInfo.InvariantCulture);
                    }

                    this.SetFieldError(String.Format(CultureInfo.CurrentCulture, NonValidDataType, value, this._row.Table.Columns[this._index].DataType.FullName));
                    break;
            }

            return value;
        }

        /// <summary>
        /// Formats value using current culture settings
        /// </summary>
        /// <param name="value">Invariant culture value</param>
        /// <param name="dataTypeName">Data type name</param>
        /// <returns>Current culture value</returns>
        private static String ToCurrentCulture(string value, string dataTypeName)
        {
            // Do nothing if there is no value for conversion
            if (string.IsNullOrEmpty(value))
            {
                return value;
            }

            // Convert value from invariant to current culture
            switch (dataTypeName)
            {
                case "Decimal":
                    Decimal decimalValue;
                    if (Decimal.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out decimalValue))
                    {
                        return decimalValue.ToString(CultureInfo.CurrentCulture);
                    }

                    break;
            }

            return value;
        }

        /// <summary>
        /// Validates data type
        /// </summary>
        /// <param name="value">Value for validation</param>
        /// <param name="dataTypeFullName">Data type full name</param>
        /// <param name="useCurrentCulture">True for using current culture, otherwise False</param>
        /// <returns>True if value is of valid data type, otherwise false</returns>
        private static bool ValidateDataType(ref string value, string dataTypeFullName, bool useCurrentCulture)
        {
            // Null is a valid value
            if (value == null)
            {
                return true;
            }

            switch (dataTypeFullName)
            {
                case "System.Boolean":
                    value = value.ToLower(CultureInfo.InvariantCulture);
                    bool boolValue;
                    if (bool.TryParse(value, out boolValue))
                    {
                        return true;
                    }

                    break;
                case "System.Byte":
                    value = value.ToLower(CultureInfo.InvariantCulture);
                    byte byteValue;
                    if (byte.TryParse(value, out byteValue))
                    {
                        return true;
                    }

                    break;
                case "System.Byte[]":
                    return true;
                case "System.DateTime":
                    DateTime dateTimeValue;
                    if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out dateTimeValue))
                    {
                        return true;
                    }

                    break;
                case "System.Decimal":
                    Decimal decimalValue;
                    if (Decimal.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out decimalValue))
                    {
                        return true;
                    }

                    break;
                case "System.Double":
                    Double doubleValue;
                    if (Double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out doubleValue))
                    {
                        return true;
                    }

                    break;
                case "System.Int16":
                    short shortValue;
                    if (short.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out shortValue))
                    {
                        return true;
                    }

                    break;
                case "System.Int32":
                    int intValue;
                    if (int.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out intValue))
                    {
                        return true;
                    }

                    break;
                case "System.Int64":
                    long longValue;
                    if (long.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out longValue))
                    {
                        return true;
                    }

                    break;
                case "System.String":
                    return true;
            }

            return false;
        }
        #endregion
    }
}
